<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> 
<html>
<head>
<title>The tetris game in wxWidgets</title>
<link rel="stylesheet" href="/cfg/format.css" type="text/css">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="keywords" content="wxWidgets, tutorial, tetris game, C++, programming, GUI, multiplatform">
<meta name="description" content="This part of the wxWidgets tutorial presents the tetris game.">
<meta name="language" content="en">
<meta name="author" content="Jan Bodnar">
<meta name="distribution" content="global">

<script type="text/javascript" src="/lib/jquery.js"></script>
<script type="text/javascript" src="/lib/common.js"></script>

</head>

<body>

<div class="container">

<div id="wide_ad" class="ltow">
<script type="text/javascript"><!--
google_ad_client = "pub-9706709751191532";
/* 160x600, August 2011 */
google_ad_slot = "2484182563";
google_ad_width = 160;
google_ad_height = 600;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>


<div class="content">


<a href="/" title="Home">Home</a>&nbsp;
<a href="..">Contents</a>


<h1>The Tetris game in wxWidgets</h1>

<p>
The Tetris game is one of the most popular computer games ever created. 
The original game was designed and programmed
by a Russian programmer <b>Alexey Pajitnov</b> in 1985. Since then, 
tetris is available on almost every computer platform in lots of variations. 
Even my mobile phone has a modified version of the Tetris game. 
</p>

<script type="text/javascript"><!--
google_ad_client = "pub-9706709751191532";
/* NewSquare */
google_ad_slot = "0364418177";
google_ad_width = 300;
google_ad_height = 250;
//-->
</script> 
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> 
</script> 

<p>
Tetris is called a falling block puzzle game. In this game, we have seven 
different shapes called <b>tetrominoes</b>.
S-shape, Z-shape, T-shape, L-shape, Line-shape, MirroredL-shape and a Square-shape.
Each of these shapes is formed with four squares. 
The shapes are falling down the board.
The object of the tetris game is to move and rotate the shapes, 
so that they fit as much as possible. 
If we manage to form a row, the row is destroyed and we score. 
We play the tetris game until we top out.
</p>


<img src="/img/gui/wxwidgets/tetrominoes.png" alt="Tetrominoes">
<div class="figure">Figure: Tetrominoes</div>

<p>
wxWidgets is a toolkit designed to create applications. There are other libraries 
which are targeted at creating computer games. Nevertheless, wxWidgets and other
application toolkits can be used to create games.
</p>


<h2>The development</h2>

<p>
We do not have images for our tetris game, we draw the tetrominoes using the drawing 
API available in the wxWidgets programming toolkit. Behind every computer game, 
there is a mathematical model. So it is in tetris. 
</p>

<p>
Some ideas behind the game.
</p>

<ul>
  <li>We use <b>wxTimer</b> to create a game cycle</li>
  <li>The tetrominoes are drawn</li>
  <li>The shapes move on a square by square basis (not pixel by pixel)</li>
  <li>Mathematically a board is a simple list of numbers</li>
</ul>

<br>

<div class="codehead">Shape.h</div>
<pre class="code">
#ifndef SHAPE_H
#define SHAPE_H

enum Tetrominoes { NoShape, ZShape, SShape, LineShape, 
                  TShape, SquareShape, LShape, MirroredLShape };

class Shape
{
public:
    Shape() { SetShape(NoShape); }
    void SetShape(Tetrominoes shape);
    void SetRandomShape();

    Tetrominoes GetShape() const { return pieceShape; }
    int x(int index) const { return coords[index][0]; }
    int y(int index) const { return coords[index][1]; }

    int MinX() const;
    int MaxX() const;
    int MinY() const;
    int MaxY() const;

    Shape RotateLeft() const;
    Shape RotateRight() const;

private:
    void SetX(int index, int x) { coords[index][0] = x; }
    void SetY(int index, int y) { coords[index][1] = y; }
    Tetrominoes pieceShape;
    int coords[4][2];
};

#endif
</pre>

<div class="codehead">Shape.cpp</div>
<pre class="code">
#include &lt;stdlib.h&gt;
#include &lt;algorithm&gt;
#include "Shape.h"

using namespace std;

void Shape::SetShape(Tetrominoes shape)
{
    static const int coordsTable[8][4][2] = {
        { { 0, 0 },   { 0, 0 },   { 0, 0 },   { 0, 0 } },
        { { 0, -1 },  { 0, 0 },   { -1, 0 },  { -1, 1 } },
        { { 0, -1 },  { 0, 0 },   { 1, 0 },   { 1, 1 } },
        { { 0, -1 },  { 0, 0 },   { 0, 1 },   { 0, 2 } },
        { { -1, 0 },  { 0, 0 },   { 1, 0 },   { 0, 1 } },
        { { 0, 0 },   { 1, 0 },   { 0, 1 },   { 1, 1 } },
        { { -1, -1 }, { 0, -1 },  { 0, 0 },   { 0, 1 } },
        { { 1, -1 },  { 0, -1 },  { 0, 0 },   { 0, 1 } }
    };

    for (int i = 0; i < 4 ; i++) {
        for (int j = 0; j < 2; ++j)
            coords[i][j] = coordsTable[shape][i][j];
    }
    pieceShape = shape;
}



void Shape::SetRandomShape()
{
  int x = rand() % 7 + 1;
  SetShape(Tetrominoes(x));
}

int Shape::MinX() const
{
  int m = coords[0][0];
  for (int i=0; i<4; i++) {
      m = min(m, coords[i][0]);
  }
  return m;
}

int Shape::MaxX() const
{
  int m = coords[0][0];
  for (int i=0; i<4; i++) {
      m = max(m, coords[i][0]);
  }
  return m;
}

int Shape::MinY() const
{
  int m = coords[0][1];
  for (int i=0; i<4; i++) {
      m = min(m, coords[i][1]);
  }
  return m;
}

int Shape::MaxY() const
{
  int m = coords[0][1];
  for (int i=0; i<4; i++) {
      m = max(m, coords[i][1]);
  }
  return m;
}

Shape Shape::RotateLeft() const
{
    if (pieceShape == SquareShape)
        return *this;

    Shape result;
    result.pieceShape = pieceShape;
    for (int i = 0; i < 4; ++i) {
        result.SetX(i, y(i));
        result.SetY(i, -x(i));
    }
    return result;
}

Shape Shape::RotateRight() const
{
    if (pieceShape == SquareShape)
        return *this;

    Shape result;
    result.pieceShape = pieceShape;
    for (int i = 0; i < 4; ++i) {
        result.SetX(i, -y(i));
        result.SetY(i, x(i));
    }
    return result;
}
</pre>

<div class="codehead">Board.h</div>
<pre class="code">
#ifndef BOARD_H
#define BOARD_H

#include "Shape.h"
#include &lt;wx/wx.h&gt;

class Board : public wxPanel
{

public:
    Board(wxFrame *parent);
    void Start();
    void Pause();
    void linesRemovedChanged(int numLines);

protected:
    void OnPaint(wxPaintEvent&amp; event);
    void OnKeyDown(wxKeyEvent&amp; event);
    void OnTimer(wxCommandEvent&amp; event);

private:
    enum { BoardWidth = 10, BoardHeight = 22 };

    Tetrominoes &amp; ShapeAt(int x, int y) { return board[(y * BoardWidth) + x]; }

    int SquareWidth() { return GetClientSize().GetWidth() / BoardWidth; }
    int SquareHeight() { return GetClientSize().GetHeight() / BoardHeight; }
    void ClearBoard();
    void DropDown();
    void OneLineDown();
    void PieceDropped();
    void RemoveFullLines();
    void NewPiece();
    bool TryMove(const Shape&amp; newPiece, int newX, int newY);
    void DrawSquare(wxPaintDC &amp;dc, int x, int y, Tetrominoes shape);

    wxTimer *timer;
    bool isStarted;
    bool isPaused;
    bool isFallingFinished;
    Shape curPiece;
    int curX;
    int curY;
    int numLinesRemoved;
    Tetrominoes board[BoardWidth * BoardHeight];
    wxStatusBar *m_stsbar;
};

#endif
</pre>

<div class="codehead">Board.cpp</div>
<pre class="code">
#include "Board.h"

Board::Board(wxFrame *parent)
       : wxPanel(parent, wxID_ANY, wxDefaultPosition,
             wxDefaultSize, wxBORDER_NONE)
{
    timer = new wxTimer(this, 1);

    m_stsbar = parent->GetStatusBar();
    isFallingFinished = false;
    isStarted = false;
    isPaused = false;
    numLinesRemoved = 0;
    curX = 0;
    curY = 0;

    ClearBoard();  

    Connect(wxEVT_PAINT, wxPaintEventHandler(Board::OnPaint));
    Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Board::OnKeyDown));
    Connect(wxEVT_TIMER, wxCommandEventHandler(Board::OnTimer));
}


void Board::Start()
{
    if (isPaused)
        return;

    isStarted = true;
    isFallingFinished = false;
    numLinesRemoved = 0;
    ClearBoard();

    NewPiece();
    timer->Start(300);
}

void Board::Pause()
{
    if (!isStarted)
        return;

    isPaused = !isPaused;
    if (isPaused) {
        timer->Stop();
        m_stsbar->SetStatusText(wxT("paused"));
    } else {
        timer->Start(300);
        wxString str;
        str.Printf(wxT("%d"), numLinesRemoved);
        m_stsbar->SetStatusText(str);
    }
    Refresh();
}

void Board::OnPaint(wxPaintEvent&amp; event)
{
    wxPaintDC dc(this);

    wxSize size = GetClientSize();
    int boardTop = size.GetHeight() - BoardHeight * SquareHeight();


    for (int i = 0; i < BoardHeight; ++i) {
        for (int j = 0; j < BoardWidth; ++j) {
            Tetrominoes shape = ShapeAt(j, BoardHeight - i - 1);
            if (shape != NoShape)
                DrawSquare(dc, 0 + j * SquareWidth(),
                           boardTop + i * SquareHeight(), shape);
        }
    }

    if (curPiece.GetShape() != NoShape) {
        for (int i = 0; i < 4; ++i) {
            int x = curX + curPiece.x(i);
            int y = curY - curPiece.y(i);
            DrawSquare(dc, 0 + x * SquareWidth(),
                       boardTop + (BoardHeight - y - 1) * SquareHeight(),
                       curPiece.GetShape());
        }
    }
}

void Board::OnKeyDown(wxKeyEvent&amp; event)
{
    if (!isStarted || curPiece.GetShape() == NoShape) {  
        event.Skip();
        return;
    }

    int keycode = event.GetKeyCode();

    if (keycode == 'p' || keycode == 'P') {
	Pause();
        return;
    }

    if (isPaused)
        return;

    switch (keycode) {
    case WXK_LEFT:
        TryMove(curPiece, curX - 1, curY);
        break;
    case WXK_RIGHT:
        TryMove(curPiece, curX + 1, curY);
        break;
    case WXK_DOWN:
        TryMove(curPiece.RotateRight(), curX, curY);
        break;
    case WXK_UP:
        TryMove(curPiece.RotateLeft(), curX, curY);
        break;
    case WXK_SPACE:
        DropDown();
        break;
    case 'd':
        OneLineDown();
        break;
    case 'D':
        OneLineDown();
        break;
    default:
        event.Skip();
    }

}

void Board::OnTimer(wxCommandEvent&amp; event)
{
    if (isFallingFinished) {
        isFallingFinished = false;
        NewPiece();
    } else {
        OneLineDown();
    }
}

void Board::ClearBoard()
{
    for (int i = 0; i < BoardHeight * BoardWidth; ++i)
        board[i] = NoShape;
}

void Board::DropDown()
{
    int newY = curY;
    while (newY > 0) {
        if (!TryMove(curPiece, curX, newY - 1))
            break;
        --newY;
    }
    PieceDropped();
}

void Board::OneLineDown()
{
    if (!TryMove(curPiece, curX, curY - 1))
        PieceDropped();
}

void Board::PieceDropped()
{
    for (int i = 0; i < 4; ++i) {
        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        ShapeAt(x, y) = curPiece.GetShape();
    }

    RemoveFullLines();

    if (!isFallingFinished)
        NewPiece();
}

void Board::RemoveFullLines()
{
     int numFullLines = 0;

     for (int i = BoardHeight - 1; i >= 0; --i) {
         bool lineIsFull = true;

         for (int j = 0; j < BoardWidth; ++j) {
             if (ShapeAt(j, i) == NoShape) {
                 lineIsFull = false;
                 break;
             }
         }

         if (lineIsFull) {
             ++numFullLines;
             for (int k = i; k < BoardHeight - 1; ++k) {
                 for (int j = 0; j < BoardWidth; ++j)
                     ShapeAt(j, k) = ShapeAt(j, k + 1);
             }
         }
     }

     if (numFullLines > 0) {
         numLinesRemoved += numFullLines;
         wxString str;
	 str.Printf(wxT("%d"), numLinesRemoved);
         m_stsbar->SetStatusText(str);

         isFallingFinished = true;
         curPiece.SetShape(NoShape);
         Refresh();
     }
 }

void Board::NewPiece()
{
    curPiece.SetRandomShape();
    curX = BoardWidth / 2 + 1;
    curY = BoardHeight - 1 + curPiece.MinY();

    if (!TryMove(curPiece, curX, curY)) {
        curPiece.SetShape(NoShape);
        timer->Stop();
        isStarted = false;
        m_stsbar->SetStatusText(wxT("game over"));
    }
}

bool Board::TryMove(const Shape&amp; newPiece, int newX, int newY)
{
    for (int i = 0; i < 4; ++i) {
        int x = newX + newPiece.x(i);
        int y = newY - newPiece.y(i);
        if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
            return false;
        if (ShapeAt(x, y) != NoShape)
            return false;
    }

    curPiece = newPiece;
    curX = newX;
    curY = newY;
    Refresh();
    return true;
}

void Board::DrawSquare(wxPaintDC&amp; dc, int x, int y, Tetrominoes shape)
{
    static wxColour colors[] = { wxColour(0, 0, 0), wxColour(204, 102, 102), 
             wxColour(102, 204, 102), wxColour(102, 102, 204), 
             wxColour(204, 204, 102), wxColour(204, 102, 204), 
             wxColour(102, 204, 204), wxColour(218, 170, 0) };

    static wxColour light[] = { wxColour(0, 0, 0), wxColour(248, 159, 171),
             wxColour(121, 252, 121), wxColour(121, 121, 252), 
             wxColour(252, 252, 121), wxColour(252, 121, 252), 
             wxColour(121, 252, 252), wxColour(252, 198, 0) };

    static wxColour dark[] = { wxColour(0, 0, 0), wxColour(128, 59, 59), 
             wxColour(59, 128, 59), wxColour(59, 59, 128), 
             wxColour(128, 128, 59), wxColour(128, 59, 128), 
             wxColour(59, 128, 128), wxColour(128, 98, 0) };


    wxPen pen(light[int(shape)]);
    pen.SetCap(wxCAP_PROJECTING);
    dc.SetPen(pen);

    dc.DrawLine(x, y + SquareHeight() - 1, x, y);
    dc.DrawLine(x, y, x + SquareWidth() - 1, y);

    wxPen darkpen(dark[int(shape)]);
    darkpen.SetCap(wxCAP_PROJECTING);
    dc.SetPen(darkpen);

    dc.DrawLine(x + 1, y + SquareHeight() - 1,
        x + SquareWidth() - 1, y + SquareHeight() - 1);
    dc.DrawLine(x + SquareWidth() - 1, 
        y + SquareHeight() - 1, x + SquareWidth() - 1, y + 1);

    dc.SetPen(*wxTRANSPARENT_PEN);
    dc.SetBrush(wxBrush(colors[int(shape)])); 
    dc.DrawRectangle(x + 1, y + 1, SquareWidth() - 2, 
        SquareHeight() - 2);
}
</pre>

<div class="codehead">Tetris.h</div>
<pre class="code">
#include &lt;wx/wx.h&gt;

class Tetris : public wxFrame
{
public:
    Tetris(const wxString&amp; title);

};
</pre>

<div class="codehead">Tetris.cpp</div>
<pre class="code">
#include "Tetris.h"
#include "Board.h"

Tetris::Tetris(const wxString&amp; title)
       : wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, wxSize(180, 380))
{
  wxStatusBar *sb = CreateStatusBar();
  sb->SetStatusText(wxT("0"));
  Board *board = new Board(this);
  board->SetFocus();
  board->Start();
}
</pre>

<div class="codehead">main.h</div>
<pre class="code">
#include &lt;wx/wx.h&gt;

class MyApp : public wxApp
{
  public:
    virtual bool OnInit();

};
</pre>

<div class="codehead">main.cpp</div>
<pre class="code">
#include "main.h"
#include "Tetris.h"


IMPLEMENT_APP(MyApp)

bool MyApp::OnInit()
{
    srand(time(NULL));
    Tetris *tetris = new Tetris(wxT("Tetris"));
    tetris->Centre();
    tetris->Show(true);

    return true;
}
</pre>

<p>
I have simplified the game a bit, so that it is easier to understand. The 
game starts immediately, after it is launched. We can pause the game by 
pressing the p key. The space key will drop the tetris piece immediately 
to the bottom. The d key will drop the piece one line down. 
(It can be used to speed up the falling a bit.) The game goes at constant 
speed, no acceleration is implemented. The score is the number of lines, 
that we have removed.
</p>

<pre class="explanation">
...
isFallingFinished = false;
isStarted = false;
isPaused = false;
numLinesRemoved = 0;
curX = 0;
curY = 0;
...
</pre>

<p>
Before we start the game, we initialize some important variables. The 
<b>isFallingFinished</b> variable determines, it the
tetris shape has finished falling and we then need to create a new shape. 
The <b>numLinesRemoved</b> counts the number of lines, we have removed so far. 
The <b>curX</b> and <b>curY</b> variables determine the actual position of the 
falling tetris shape. 
</p>


<pre class="explanation">
for (int i = 0; i < BoardHeight; ++i) {
    for (int j = 0; j < BoardWidth; ++j) {
        Tetrominoes shape = ShapeAt(j, BoardHeight - i - 1);
        if (shape != NoShape)
            DrawSquare(dc, 0 + j * SquareWidth(),
                    boardTop + i * SquareHeight(), shape);
    }
}
</pre>

<p>
The painting of the game is divided into two steps. In the first step, we draw 
all the shapes, or remains of the shapes, that have been dropped to the bottom 
of the board. All the squares are rememberd in the <b>board</b> array. We 
access it using the <b>ShapeAt()</b> method.
</p>

<pre class="explanation">
 if (curPiece.GetShape() != NoShape) {
     for (int i = 0; i < 4; ++i) {
         int x = curX + curPiece.x(i);
         int y = curY - curPiece.y(i);
         DrawSquare(dc, 0 + x * SquareWidth(),
                    boardTop + (BoardHeight - y - 1) * SquareHeight(),
                    curPiece.GetShape());
     }
 }
</pre>

<p>
The next step is drawing of the actual piece, that is falling down.
</p>

<pre class="explanation">
 ...
 switch (keycode) {
 case WXK_LEFT:
     TryMove(curPiece, curX - 1, curY);
     break;
 ...
</pre>

<p>
In the <b>Board::OnKeyDown()</b> method we check for pressed keys. If we 
press the left arrow key, we try to move the piece to the left. We say 
try, because the piece might not be able to move.
</p>


<pre class="explanation">
void Board::OnTimer(wxCommandEvent&amp; event)
{
    if (isFallingFinished) {
        isFallingFinished = false;
        NewPiece();
    } else {
        OneLineDown();
    }
}
</pre>

<p>
In the <b>Board::OnTimer()</b> method we either create a new piece, after 
the previous one was dropped to the bottom, or we move a falling piece 
one line down.
</p>

<pre class="explanation">
void Board::DropDown()
{
    int newY = curY;
    while (newY > 0) {
        if (!TryMove(curPiece, curX, newY - 1))
            break;
        --newY;
    }
    PieceDropped();
}
</pre>

<p>
The <b>Board::DropDown()</b> method drops the falling shape immediately 
to the bottom of the board. It happens, when 
we press the space key.
</p>


<pre class="explanation">
void Board::PieceDropped()
{
    for (int i = 0; i < 4; ++i) {
        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        ShapeAt(x, y) = curPiece.GetShape();
    }

    RemoveFullLines();

    if (!isFallingFinished)
        NewPiece();
}
</pre>

<p>
In the <b>Board::PieceDropped()</b> method we set the current shape 
at it's final position. We call the <b>RemoveFullLines()</b> 
method to check, if we have at least one full line. And we create a new tetris shape, if it 
wasn't already created in the <b>Board::PieceDropped()</b> method in the meantime.
</p>

<pre class="explanation">
 if (lineIsFull) {
     ++numFullLines;
     for (int k = i; k < BoardHeight - 1; ++k) {
         for (int j = 0; j < BoardWidth; ++j)
             ShapeAt(j, k) = ShapeAt(j, k + 1);
     }
 }
</pre>

<p>
This code removes the full lines. After finding a full line we increase 
the counter. We move all the lines above the 
full row one line down. This way we destroy the full line. Notice, that 
in our tetris game, we use so called 
<b>naive gravity</b>. This means, that the squares may be left floating 
above empty gaps.
</p>

<pre class="explanation">
void Board::NewPiece()
{
    curPiece.SetRandomShape();
    curX = BoardWidth / 2 + 1;
    curY = BoardHeight - 1 + curPiece.MinY();

    if (!TryMove(curPiece, curX, curY)) {
        curPiece.SetShape(NoShape);
        timer->Stop();
        isStarted = false;
        m_stsbar->SetStatusText(wxT("game over"));
    }
}
</pre>

<p>
The <b>Board::NewPiece()</b> method creates randomly a new tetris piece. 
If the piece cannot go into it's initial position, the game is over.
</p>

<pre class="explanation">
bool Board::TryMove(const Shape&amp; newPiece, int newX, int newY)
{
    for (int i = 0; i < 4; ++i) {
        int x = newX + newPiece.x(i);
        int y = newY - newPiece.y(i);
        if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
            return false;
        if (ShapeAt(x, y) != NoShape)
            return false;
    }

    curPiece = newPiece;
    curX = newX;
    curY = newY;
    Refresh();
    return true;
}
</pre>

<p>
In the <b>Board::TryMove()</b> method we try to move our shapes. If the shape 
is at the edge of the board or is adjacent to some other shape, we return false. 
Otherwise we place the current falling shape to a new position and return true.
</p>


<p>
The <b>Shape</b> class saves information about the tetris piece. 
</p>


<pre class="explanation">
for (int i = 0; i < 4 ; i++) {
    for (int j = 0; j < 2; ++j)
        coords[i][j] = coordsTable[shape][i][j];
}
</pre>

<p>
The coords array saves the coordinates of the tetris piece.
For example, numbers { 0, -1 },  { 0, 0 },   { 1, 0 },   { 1, 1 } , represent a 
rotated S-shape. The following diagram illustrates the shape.
</p>

<img src="/img/gui/wxwidgets/coordinates.png" alt="Coordinates">
<div class="figure">Figure: Coordinates</div>

<p>
When we draw the current falling piece, we draw it at  <b>curX</b>, <b>curY</b> position. 
Then we look at the coordinates table and draw all the four squares. 
</p>


<img src="/img/gui/wxwidgets/tetris.png" alt="Tetris">
<div class="figure">Figure: Tetris</div>


<p>
This was a Tetris game in wxWidgets.
</p>

<div class="center"> 
<script type="text/javascript"><!--
google_ad_client = "pub-9706709751191532";
/* horizontal */
google_ad_slot = "1734478269";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script> 
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> 
</script> 
</div> 
<br>


<div class="botNav, center">
<span class="botNavItem"><a href="/">Home</a></span> ‡ <span class="botNavItem"><a href="..">Contents</a></span> ‡
<span class="botNavItem"><a href="#">Top of Page</a></span>
</div>


<div class="footer">
<div class="signature">
<a href="/">ZetCode</a> last modified December 9, 2007  <span class="copyright">&copy; 2007 - 2013 Jan Bodnar</span>
</div>
</div>

</div> <!-- content -->

</div> <!-- container -->

</body>
</html>
